﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.ComponentModel;
using System.Globalization;
using static System.Windows.Forms.ComboBox.ObjectCollection;

namespace System.Windows.Forms;

public partial class ComboBox
{
    [ListBindable(false)]
    public partial class ObjectCollection : IList, IComparer<Entry>
    {
        private readonly ComboBox _owner;
        private ComboBoxAccessibleObject? _ownerComboBoxAccessibleObject;
        private List<Entry>? _innerList;

        public ObjectCollection(ComboBox owner)
        {
            _owner = owner;
        }

        private ComboBoxAccessibleObject? OwnerComboBoxAccessibleObject
        {
            get
            {
                if (!_owner.IsAccessibilityObjectCreated)
                {
                    return null;
                }

                if (_ownerComboBoxAccessibleObject is not null)
                {
                    return _ownerComboBoxAccessibleObject;
                }

                if (_owner.AccessibilityObject is ComboBoxAccessibleObject accessibleObject)
                {
                    _ownerComboBoxAccessibleObject = accessibleObject;
                }

                return _ownerComboBoxAccessibleObject;
            }
        }

        internal List<Entry> InnerList
        {
            get
            {
                _innerList ??= [];

                return _innerList;
            }
        }

        /// <summary>
        ///  Retrieves the number of items.
        /// </summary>
        public int Count => InnerList.Count;

        object ICollection.SyncRoot
        {
            get
            {
                return this;
            }
        }

        bool ICollection.IsSynchronized
        {
            get
            {
                return false;
            }
        }

        bool IList.IsFixedSize
        {
            get
            {
                return false;
            }
        }

        public bool IsReadOnly
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        ///  Adds an item to the combo box. For an unsorted combo box, the item is
        ///  added to the end of the existing list of items. For a sorted combo box,
        ///  the item is inserted into the list according to its sorted position.
        ///  The item's toString() method is called to obtain the string that is
        ///  displayed in the combo box.
        ///  A SystemException occurs if there is insufficient space available to
        ///  store the new item.
        /// </summary>
        public int Add(object item)
        {
            _owner.CheckNoDataSource();
            int index = AddInternal(item);
            if (_owner.UpdateNeeded() && _owner.AutoCompleteSource == AutoCompleteSource.ListItems)
            {
                _owner.SetAutoComplete(false, false);
            }

            return index;
        }

        private int AddInternal(object item)
        {
            ArgumentNullException.ThrowIfNull(item);

            int index = -1;
            if (!_owner._sorted)
            {
                InnerList.Add(new Entry(item));
            }
            else
            {
                Entry entry = item is Entry entryItem ? entryItem : new Entry(item);
                index = InnerList.BinarySearch(index: 0, Count, entry, this);
                if (index < 0)
                {
                    index = ~index; // getting the index of the first element that is larger than the search value
                }

                Debug.Assert(index >= 0 && index <= Count, "Wrong index for insert");
                InnerList.Insert(index, entry);
            }

            bool successful = false;

            try
            {
                if (_owner._sorted)
                {
                    if (_owner.IsHandleCreated)
                    {
                        _owner.NativeInsert(index, item);
                    }
                }
                else
                {
                    index = Count - 1;
                    if (_owner.IsHandleCreated)
                    {
                        _owner.NativeAdd(item);
                    }
                }

                successful = true;
            }
            finally
            {
                if (!successful)
                {
                    OwnerComboBoxAccessibleObject?.ItemAccessibleObjects.Remove(InnerList[index]);
                    Remove(item);
                }
            }

            return index;
        }

        int IList.Add(object? item)
        {
            return Add(item!);
        }

        public void AddRange(params object[] items)
        {
            _owner.CheckNoDataSource();
            _owner.BeginUpdate();
            try
            {
                AddRangeInternal(items);
            }
            finally
            {
                _owner.EndUpdate();
            }
        }

        internal void AddRangeInternal(IList items)
        {
            ArgumentNullException.ThrowIfNull(items);

            foreach (object item in items)
            {
                // adding items one-by-one for performance (especially for sorted ComboBox)
                // we can not rely on ArrayList.Sort since its worst case complexity is n*n
                // AddInternal is based on BinarySearch and ensures n*log(n) complexity
                AddInternal(item);
            }

            if (_owner.AutoCompleteSource == AutoCompleteSource.ListItems)
            {
                _owner.SetAutoComplete(false, false);
            }
        }

        /// <summary>
        ///  Retrieves the item with the specified index.
        /// </summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public virtual object? this[int index]
        {
            get
            {
                ArgumentOutOfRangeException.ThrowIfNegative(index);
                ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);

                return InnerList[index].Item;
            }
            set
            {
                _owner.CheckNoDataSource();
                SetItemInternal(index, value!);
            }
        }

        /// <summary>
        ///  Removes all items from the ComboBox.
        /// </summary>
        public void Clear()
        {
            _owner.CheckNoDataSource();
            ClearInternal();
        }

        internal void ClearInternal()
        {
            if (_owner.IsHandleCreated)
            {
                _owner.NativeClear();
            }

            InnerList.Clear();
            OwnerComboBoxAccessibleObject?.ResetListItemAccessibleObjects();

            _owner._selectedIndex = -1;
            if (_owner.AutoCompleteSource == AutoCompleteSource.ListItems)
            {
                _owner.SetAutoComplete(false, true /*recreateHandle*/);
            }
        }

        public bool Contains(object? value)
        {
            return IndexOf(value) != -1;
        }

        /// <summary>
        ///  Copies the ComboBox Items collection to a destination array.
        /// </summary>
        public void CopyTo(object[] destination, int arrayIndex)
        {
            ArgumentNullException.ThrowIfNull(destination);

            int count = InnerList.Count;

            ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(arrayIndex, destination.Length - count);

            for (int i = 0; i < count; i++)
            {
                destination[i + arrayIndex] = InnerList[i].Item;
            }
        }

        void ICollection.CopyTo(Array destination, int index)
        {
            ArgumentNullException.ThrowIfNull(destination);

            int count = InnerList.Count;

            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(index, destination.Length - count);

            for (int i = 0; i < count; i++)
            {
                destination.SetValue(InnerList[i], i + index);
            }
        }

        /// <summary>
        ///  Returns an enumerator for the ComboBox Items collection.
        /// </summary>
        public IEnumerator GetEnumerator() => new EntryEnumerator(InnerList);

        /// <summary>
        ///  Adds an item to the combo box. For an unsorted combo box, the item is
        ///  added to the end of the existing list of items. For a sorted combo box,
        ///  the item is inserted into the list according to its sorted position.
        ///  The item's toString() method is called to obtain the string that is
        ///  displayed in the combo box.
        ///  A SystemException occurs if there is insufficient space available to
        ///  store the new item.
        /// </summary>
        public void Insert(int index, object? item)
        {
            _owner.CheckNoDataSource();

            ArgumentNullException.ThrowIfNull(item);
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(index, Count);

            // If the combo box is sorted, then nust treat this like an add
            // because we are going to twiddle the index anyway.
            //
            if (_owner._sorted)
            {
                Add(item);
            }
            else
            {
                InnerList.Insert(index, new Entry(item));
                if (_owner.IsHandleCreated)
                {
                    bool successful = false;

                    try
                    {
                        _owner.NativeInsert(index, item);
                        successful = true;
                    }
                    finally
                    {
                        if (successful)
                        {
                            if (_owner.AutoCompleteSource == AutoCompleteSource.ListItems)
                            {
                                _owner.SetAutoComplete(false, false);
                            }
                        }
                        else
                        {
                            OwnerComboBoxAccessibleObject?.RemoveComboBoxItemAccessibleObjectAt(index);
                            InnerList.RemoveAt(index);
                        }
                    }
                }
            }
        }

        /// <summary>
        ///  Removes an item from the ComboBox at the given index.
        /// </summary>
        public void RemoveAt(int index)
        {
            _owner.CheckNoDataSource();

            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);

            if (_owner.IsHandleCreated)
            {
                _owner.NativeRemoveAt(index);
            }

            OwnerComboBoxAccessibleObject?.RemoveComboBoxItemAccessibleObjectAt(index);
            InnerList.RemoveAt(index);

            if (!_owner.IsHandleCreated)
            {
                if (index < _owner._selectedIndex)
                {
                    _owner._selectedIndex--;
                }
                else if (index == _owner._selectedIndex)
                {
                    _owner._selectedIndex = -1;
                    _owner.UpdateText();
                }
            }

            if (_owner.AutoCompleteSource == AutoCompleteSource.ListItems)
            {
                _owner.SetAutoComplete(false, false);
            }
        }

        /// <summary>
        ///  Removes the given item from the ComboBox, provided that it is
        ///  actually in the list.
        /// </summary>
        public void Remove(object? value)
        {
            int index = IndexOf(value);

            if (index != -1)
            {
                RemoveAt(index);
            }
        }

        internal void SetItemInternal(int index, object value)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(index);
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);

            InnerList[index].Item = value.OrThrowIfNull();

            // If the native control has been created, and the display text of the new list item object
            // is different to the current text in the native list item, recreate the native list item...
            if (!_owner.IsHandleCreated)
            {
                return;
            }

            bool selected = (index == _owner.SelectedIndex);

            if (string.Compare(_owner.GetItemText(value), _owner.NativeGetItemText(index), true, CultureInfo.CurrentCulture) != 0)
            {
                _owner.NativeRemoveAt(index);
                _owner.NativeInsert(index, value);
                if (selected)
                {
                    _owner.SelectedIndex = index;
                    _owner.UpdateText();
                }

                if (_owner.AutoCompleteSource == AutoCompleteSource.ListItems)
                {
                    _owner.SetAutoComplete(false, false);
                }
            }
            else
            {
                // NEW - FOR COMPATIBILITY REASONS
                // Minimum compatibility fix
                if (selected)
                {
                    _owner.OnSelectedItemChanged(EventArgs.Empty);   // we do this because set_SelectedIndex does this. (for consistency)
                    _owner.OnSelectedIndexChanged(EventArgs.Empty);
                }
            }
        }

        public int IndexOf(object? value)
        {
            int virtualIndex = -1;

            foreach (Entry entry in InnerList)
            {
                virtualIndex++;
                if ((value is Entry itemEntry && entry == itemEntry) || entry.Item.Equals(value))
                {
                    return virtualIndex;
                }
            }

            return -1;
        }

        int IComparer<Entry>.Compare(Entry? entry1, Entry? entry2)
        {
            if (IComparerHelpers.CompareReturnIfNull(entry1, entry2, out int? returnValue))
            {
                return (int)returnValue;
            }

            string? itemName1 = _owner.GetItemText(entry1.Item);
            string? itemName2 = _owner.GetItemText(entry2.Item);

            CompareInfo compInfo = Application.CurrentCulture.CompareInfo;
            return compInfo.Compare(itemName1, itemName2, CompareOptions.StringSort);
        }
    }
}
